package me.chenqiang.cinc;

import java.math.BigInteger;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

public class CincCommons {
	/**
	 * 对中国身份证号和永久居民身份证号进行压缩表示的类。
	 * 使用了0-9A-Z中除O、I、S、Z以外的其他字符（手写和目视时容易与0、1、2、5混淆），共32个字符。
	 * 每个字符可以表示5bit。
	 */
	private CincCommons() {}
	
	public static final String SYMBOLS = "0123456789ABCDEFGHJKLMNPQRTUVWXY"; //不使用O、I、S、Z减少手写和识读混淆，共32个字符
	public static final int BASE;
	public static final Map<Character, Integer> SYMBOL_TO_NUMBER;
	public static final Pattern SYMBOLS_PATTERN;	
	public static final LocalDate CENTURY20 = LocalDate.of(1900, 1, 1);	
	public static final Pattern SYMBOLS_9_PATTERN;
	
	static {
		BASE = SYMBOLS.length();
		HashMap<Character, Integer> s2n = new HashMap<>();
		for(int i = 0; i < SYMBOLS.length(); i++) {
			s2n.put(SYMBOLS.charAt(i), i);
		}		
		SYMBOL_TO_NUMBER = Collections.unmodifiableMap(s2n);
		SYMBOLS_PATTERN = Pattern.compile(String.format("[%s]+", SYMBOLS));
		SYMBOLS_9_PATTERN = Pattern.compile(String.format("[%s]{1,9}", SYMBOLS));
		
		assert BASE == 32;
	}
	
	/**
	 * 将数字转换为32进制字符串表示。
	 * 
	 * @param number 输入数字
	 * @param width 输出字符串长度
	 * @param fill 长度不足时用于补白的字符
	 * @return 输出字符串
	 * @throws CincCodingException
	 */
	public static String toSymbols(long number, int width, char fill) throws CincCodingException {
		if(number < 0) {
			throw new CincCodingException(String.format("编码数字%d小于零", number));
		}
		StringBuilder sb = new StringBuilder();
		long remaining = number;
		while(remaining > 0) {
			sb.append(SYMBOLS.charAt((int)(remaining & 0x1f))); // number % BASE
			remaining >>>= 5; // number / BASE
		}
		if(sb.length() > width) {
			throw new CincCodingException(String.format("数字%d超出了%d位符号表示的上限", number, width));
		}
		if(sb.length() < width) {
			int toFill = width - sb.length();
			for(int i = 0; i < toFill; i++) {
				sb.append(fill);
			}
		}		
		return sb.reverse().toString();
	}
	
	public static String toSymbols(BigInteger number, int width, char fill) throws CincCodingException {
		if(number.signum() < 0) {
			throw new CincCodingException(String.format("编码数字%s小于零", number));
		}
		StringBuilder sb = new StringBuilder();
		BigInteger remaining = number;
		while(remaining.signum() > 0) {
			BigInteger [] divided = remaining.divideAndRemainder(BigInteger.valueOf(0x20));
			sb.append(SYMBOLS.charAt(divided[1].intValueExact())); // number % BASE
			remaining = divided[0]; // number / BASE
		}
		if(sb.length() > width) {
			throw new CincCodingException(String.format("数字%d超出了%d位符号表示的上限", number, width));
		}
		if(sb.length() < width) {
			int toFill = width - sb.length();
			for(int i = 0; i < toFill; i++) {
				sb.append(fill);
			}
		}		
		return sb.reverse().toString();
	}
	
	/**
	 * 将32进制字符串转换为数字。
	 * @param symbols 输入字符串
	 * @return 输出数字
	 * @throws CincCodingException
	 */
	public static long toNumbers(String symbols) throws CincCodingException {
		if(!SYMBOLS_9_PATTERN.matcher(symbols).matches() ) {
			throw new CincCodingException(String.format("压缩编码%s格式不符合规范", symbols));
		}
		long res = 0;
		for(int i = 0; i < symbols.length(); i ++) {
			res = res << 5 | SYMBOL_TO_NUMBER.get(symbols.charAt(i)); // res = res * BASE + SYMBOL_TO_NUMBER.get(symbols.charAt(i))
		}
		return res;
	}
	
	public static BigInteger toBigInteger(String symbols) throws CincCodingException {
		if(!SYMBOLS_PATTERN.matcher(symbols).matches() ) {
			throw new CincCodingException(String.format("压缩编码%s格式不符合规范", symbols));
		}
		BigInteger res = BigInteger.ZERO;
		for(int i = 0; i < symbols.length(); i ++) {
			res = res.shiftLeft(5).or(BigInteger.valueOf(SYMBOL_TO_NUMBER.get(symbols.charAt(i))));
		}
		return res;
	}
	
	public static long calculateDaysFrom1900(String datestr) throws CincCodingException {
		try {
			LocalDate birthDate = LocalDate.parse(datestr, DateTimeFormatter.BASIC_ISO_DATE);
			return ChronoUnit.DAYS.between(CincCommons.CENTURY20, birthDate);			
		}
		catch(DateTimeParseException dpe) {
			throw new CincCodingException(datestr);
		}
	}
	
	public static String calculateDateByDaysFrom1900(long days) {
		LocalDate birthDate = ChronoUnit.DAYS.addTo(CincCommons.CENTURY20, days);
		return DateTimeFormatter.BASIC_ISO_DATE.format(birthDate);
	}
	
	protected static final int [] CHECK_WEIGHTS = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};
	protected static final char [] CHECK_CODES = {'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'};
	protected static final Pattern NUMBER_TO_CHECK = Pattern.compile("\\d{17}");
	public static char calculateCheckCode(CharSequence number) {
		if(NUMBER_TO_CHECK.matcher(number) == null) {
			throw new IllegalArgumentException();
		}
		int sum = 0;		
		
		for(int i = 0; i < 17; i++) {
			sum += (CHECK_WEIGHTS[i] * (number.charAt(i) - 0x30));
		}
		return CHECK_CODES[sum % 11]; 
	}
	
	protected static final int [] ICAO_9303_WEIGHTS = {7, 3, 1};
	protected static final Pattern ICAO_9303_TO_CHECK = Pattern.compile("[0-9A-Z]+");
	public static char calcualteICAO9303CheckCode(CharSequence number) {
		int sum = 0;
		
		for(int i = 0; i < number.length(); i++) {
			int n = number.charAt(i);
			if (n >=0x30 && n <=0x39) {
				n -= 0x30;
			}
			else {
				n -= 0x37;
			}
			sum += n * ICAO_9303_WEIGHTS[i % 3];
		}
		return (char)(sum % 10 + 0x30);
	}
}
